9장 단위 테스트
Intro
이 장에서는 단위 테스트에 대해 다룬다.
과거엔 드라이버 코드를 급조해 결과물이 나오는 것을 팀원들에게 보여줘야 했다.
그리고 버렸다.
하지만 현재는 애자일과 TDD 덕택에 단위 테스트를 자동화하는 프로그래머들이 많아졌다.
그러던 중 많은 프로그래머들이 놓친 미묘한 사실을 알아가보자.
TDD 법칙 세 가지
- 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
위 세 가지 법칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다.
또한 매일 많은 양에 달하는 테스트 케이스가 나온다.
사실상 전부 테스트하는 테스트 케이스가 나온다.
하지만 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
깨끗한 테스트 코드 유지하기
"테스트 코드는 실제 코드 못지 않게 중요하다."저자는 테스트 코드에 팀원들간 규칙을 깨도 좋다고 허용한 팀을 예시로 든다.
테스트 코드를 잘 짜는 것보다, 안 짜는 것보다 짜는 것이 좋다고 판단했을 것이다.
실제 코드가 변할 때 테스트 코드도 변한다. 그런데테스트 코드가 지저분할 수록 실제 코드를 변경하기 어렵다.
테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 시간이 더 걸린다.
실제 코드를 변경해 기존 테스트 케이스가 실패하기 시작하면,
지저분한 테스트 코드로 인해, 실패하는 테스트 케이스를 점점 통과시키기 어려워진다.
테스트 슈트가 없으면 개발자는 검증하지 못한다.
결국 결함율이 높아진다.
의도하지 않은 결함이 많아지면, 변경을 주저한다.
변경하면 손해가 크다 생각해 더 이상 코드를 정리하지 않는다.
그러면서 코드가 망가지기 시작한다.
결국 테스트 슈트도 없고, 얼기설기 뒤섞인 코드에, 좌절한 고객과, 테스트에 쏟아 부은 노력이 허사였다는 실망감만 남는다.
그러므로 테스트 코드를 실제 코드 못지 않게 깨끗하게 짜야한다.
테스트는 유연성, 유지보수성, 재사용성을 제공한다.
코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다.
아무리 아키텍처가 유연하고, 설계를 잘 나눠도, 테스트 케이스가 없으면 개발자는 변경을 주저한다.
버그가 숨어들까 두렵기 때문이다.
하지만 테스트 케이스가 있다면 괜찮다.
테스트 케이스가 제공하는 것들로 인해 변경이 쉬워진다.
따라서 테스트 코드가 지저분하면 코드를 변경하는 능력이 떨어지며
코드 구조를 개선하는 능력도 떨어진다.
테스트 코드가 지저분할수록 실제 코드도 지저분해진다.
테스트 코드를 잃어버리고 실제 코드도 망가진다.
깨끗한 테스트 코드
깨끗한 테스트 코드를 만드는 데 가장 중요한 것은 가독성이다.
명료성, 단순성, 풍부한 표현력이 필요하다.
저자는 여기서 테스트 코드를 예시로 든다.
addPage와 assertSubString을 부르느라 중복이 되는 코드가 많은 코드를 말이다.
여기선 BUILD-OPERATE-CHECK 패턴이 적합하다.
- BUILD: 테스트 자료를 만든다.
- OPERATE: 테스트 자료를 조작한다.
- CHECK: 조작한 결과가 올바른지 확인한다.
GIVEN-WHEN-THEN 패턴과 비슷하다.
도메인에 특화된 테스트 언어(DSL)
흔히 쓰는 시스템 조작 API를 사용하는 대신
API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용하므로
테스트 코드를 짜기도 읽기도 쉬워진다.
이러한 코드는 코드를 계속 리팩터링하다가 진화된 API다.
이중 표준
저자는 온도가 ‘급격하게 떨어지면’ 경보, 온풍기, 송풍기가
모두 가동되는지 확인하는 코드를 예시로 든다.
정말 신기한 사실은 저자가 말한대로 내가 코드를 읽었다는 것이고 그게 피곤했다는 것이다.
저자는 코드를 함수로 감추고, 정보를 간결하게 표현함으로써 해결한다.
하지만 그릇된 정보를 피하라는 규칙의 위반이지만, 현 상황에는 적절하다.
다른 예시로는 제한적인 환경에 대해 말한다.
임베디드 시스템에서 컴퓨터 자원과 메모리가 제한적일 가능성이 많다.
하지만 테스트 환경은 자원이 제한적일 가능성이 낮다.
즉, 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제없는 방식이 있다.
대개 메모리나 CPU 효율과 관련 있는 경우다.
코드의 깨끗함과는 철저히 무관하다.
테스트당 assert 하나
JUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있다.
그래서 GIVEN-WHEN-THEN 이라는 관례를 많이 사용한다.
이로써 테스트 코드를 읽기 쉬워지지만, 테스트를 분리하면 중복되는 코드가 많아진다.
TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다.
given/when 부분을 부모 클래스에 두고, then 부분을 자식 클래스에 두면 된다.
혹은 given/when 부분을 @Before 함수에 두고, then 부분을 @Test 함수에 둬도 된다.
하지만 배보다 배꼽이 더 크다.
때로는 함수 하나에 assert 문을 여러 개 넣기도 하지만 assert 문 개수는 최대한 줄여야 좋겠다.
테스트당 개념 하나
"테스트 함수마다 한 개념만 테스트하라."이 규칙이 더 낫겠다.
저자는 여기서 독자적인 개념 세 개를 테스트하는 코드를 예시로 든다.
세 개념을 한 함수로 몰아넣으면,
각 절이 존재하는 이유와 각 절이 테스트하는 개념을 모두 이해해야 한다.
하지만 assert 문이 여럿이라는 사실이 문제가 아니다.
따라서 한 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제다.
그러므로 가장 좋은 규칙은 개념 당 assert 문 수를 최소로 줄여라와
테스트 함수 하나는 개념 하나만 테스트 하라라 하자.
F.I.R.S.T.
깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.
- Fast(빠르게): 자주 돌리지 않으면 초반에 문제를 찾아내 고치지 못한다. 코드를 마음껏 정리하지도 못한다. 결국 코드 품질이 망가지기 시작한다.
- Independent(독립적으로): 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다.
- Repeatable(반복가능하게): 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다. 게다가 환경이 지원되지 않기에 테스트를 수행하지 못하는 상황에 직면한다.
- Self-Validating(자가검증하는): 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다.
- Timely(적시에): 실제 코드가 테스트하기 어렵다는 사실을 발견할지 모른다. 어떤 실제 코드는 테스트하기 너무 어렵다고 판명날지 모른다. 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.
결론
테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화한다.
그러므로 테스트 코드는 지속적으로 깨끗하게 관리하자.
표현력을 높이고 간결하게 정리하자.
테스트 API를 구현해 도메인 특화 언어(Domain Specific Language)를 만들자.
또한 모르던 개념을 알게 되었다.
- 웹 로봇(크롤러): 사람과의 상호작용 없이 연속된 웹 트랜잭션들을 자동으로 수행하는 소프트웨어 프로그램이다. 콘텐츠를 가져오고, 하이퍼링크를 따라가고, 발견한 데이터를 처리한다.
- BUILD-OPERATE-CHECK 패턴: given-when-then이랑 같은 의미
- TEMPLATE METHOD 패턴: 여러 클래스에서 공통으로 사용하는 매서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴
직접 테스트 코드를 짜본 것은 강의를 따라한 정도다.
내가 일하는 곳에서는 테스트 코드를 작성하지 않고, 바로 유저 테스트로 넘어간다.
여기서 나는 항상 결함에 대한 불안감이 있었다.
유지보수를 하면서도 항상 불안해 있다.
변경에 주저한다는 것과 코드를 다시 안 보게 된다는 말에 동감한다.
이번 글을 통해 깨끗한 테스트 코드의 중요성을 알게 되었고, 회사에 적용할 수 있는 방법을 강구해봐야겠다.